﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Transactions;
using CoreEx.Common.Data;
using CoreEx.Common.Extensions;
using CoreEx.Common.Proxy;
using CoreEx.Database.Metadata;
using CoreEx.DynamicInvoker;
using LinFu.IoC.Configuration;
using LinFu.IoC.Interfaces;
using LinFu.IoC;
using CoreEx.Database.Query;

namespace CoreEx.Database.Linq.Implementation
{
    [Implements(typeof(IMaterializer), ServiceName = "ObjectMaterializer")]
    public class ObjectMaterializer : IMaterializer, IInitialize
    {
        private IServiceContainer _serviceContainer;        
        
        private IDictionary<PropertyInfo, PropertyInfo> _ownerPropertMap = 
            new Dictionary<PropertyInfo, PropertyInfo>();

        private readonly IDictionary<Type,IDictionary<SurrogateKey,object>> _cache = 
            new Dictionary<Type, IDictionary<SurrogateKey, object>>();


        private IList _result;

        private IDictionary<Type, Type> _typeMap = new Dictionary<Type, Type>();

        private readonly IDictionary<Type, ConstructorInfo> _constructors = new Dictionary<Type, ConstructorInfo>();


        public void Materilize(ITranslateResult_old translateResult)
        {                        
            var projectedType = translateResult.ProjectedType;
            _result = CreateResultList(projectedType);
            DoMaterialize(translateResult);
            translateResult.Result = _result;
            _cache.Clear();
        }

        private void DoMaterialize(ITranslateResult_old translateResult)
        {
            var dbCommand = _serviceContainer.GetService<IDbCommand>();
            IDataReader reader;
            if (Transaction.Current != null)
                reader = dbCommand.ExecuteReader((IQueryExpression)translateResult.DbExpression);
            else
                reader = dbCommand.ExecuteReader((IQueryExpression)translateResult.DbExpression,CommandBehavior.CloseConnection);
            
            Trace.WriteLine(string.Format("Start materializing (includes reader.Read())result from query: {0}. Allocated Managed Memory : {1}", translateResult.DbExpression,GC.GetTotalMemory(false)));            
            while (reader.Read())
            {                
                ProcessTranslateResult(reader, translateResult);                
            }
            reader.Close();
            Trace.WriteLine(string.Format("Finished materializing (includes reader.Read()) result from query: {0}. Allocated Managed Memory : {1}", translateResult.DbExpression, GC.GetTotalMemory(false)));
            foreach (var memberResult in translateResult.MemberResults.Values)
            {
                DoMaterialize(memberResult);
            }
        }


        private IList CreateResultList(Type listType)
        {
            var genericListType = typeof (List<>).MakeGenericType(listType);
            return (IList)Activator.CreateInstance(genericListType);
        }


        private void ProcessTranslateResult(IDataRecord dataRecord, ITranslateResult_old translateResult)
        {                        
                var projectionTarget = translateResult.ProjectionTargets.First().Value;
                var instance = ProcessProjectionTarget(dataRecord, projectionTarget);                
                if (instance == null)
                    return;
                if (translateResult.RelationLoadTarget != null)
                {
                    var property = translateResult.RelationLoadTarget.TargetProperty;
                    var foreignKeyColumns =
                        translateResult.RelationLoadTarget.PropertyMapping.ForeignKeyInfo.Columns.Select(
                            c => c.ForeignColumnInfo.ColumnName).ToArray();

                    var foreignKeyValues = GetKeyValues(dataRecord, foreignKeyColumns);
                    var foreignSurrogateKey = new SurrogateKey(foreignKeyValues);
                    var parentCache = _cache[property.DeclaringType];
                    object parentInstance;
                    parentCache.TryGetValue(foreignSurrogateKey, out parentInstance);
                    if (parentInstance != null)
                    {
                        var list = (IList) property.DynamicInvoke(parentInstance);
                        list.Add(instance);

                        var ownerProperty = GetOwnerProperty(property);
                        if (ownerProperty != null)
                        {
                            ownerProperty.DynamicInvoke(instance, parentInstance);
                        }
                    }
                }
                else
                {
                    _result.Add(instance);
                }
        }

        private PropertyInfo GetOwnerProperty(PropertyInfo propertyInfo)
        {
            
            if (!_ownerPropertMap.ContainsKey(propertyInfo))
            {
                var targetType = propertyInfo.PropertyType.GetGenericElementType();
                var ownerProperty = targetType.GetProperties().Where(p => p.PropertyType == propertyInfo.DeclaringType).FirstOrDefault();
                _ownerPropertMap.Add(propertyInfo,ownerProperty);
            }

            return _ownerPropertMap[propertyInfo];            

        }



        private object ProcessProjectionTarget(IDataRecord dataRecord,IProjectionTarget_old projectionTarget)
        {
            var targetType = projectionTarget.GetMemberType();
            
            SurrogateKey surrogateKey = CreatePrimarySurrogateKey(dataRecord, projectionTarget);  
            if (surrogateKey == null)
                return null;

            object instance = null;

            //Possible solution for the slef join problem is 
            // to map child targets regardless of the object beeing in cache or not.
            if (IsCached(surrogateKey, targetType))
                instance = _cache[targetType][surrogateKey];
            else
            {
                instance = CreateInstance(targetType);
                foreach (var map in projectionTarget.MemberMap)
                {
                    var value = dataRecord[map.Value];
                    if (value == DBNull.Value)
                        value = null;
                    ((PropertyInfo) map.Key).DynamicInvoke(instance, value);
                }

                AddToCache(surrogateKey, instance, targetType);
            }
            foreach (var child in projectionTarget.Children.Where(c => c.Projection != null))
            {
                var childInstance = ProcessProjectionTarget(dataRecord, child);
                if (childInstance != null)
                {
                    ((PropertyInfo) child.Member).DynamicInvoke(instance, childInstance);
                    var propertyMappingInfo =
                        projectionTarget.TypeMappingInfo.ColumnMappings[(PropertyInfo) child.Member];
                    if (propertyMappingInfo.RelationType == CardinalityType.OneToOne)
                    {
                        var ownerProperty = GetOwnerProperty((PropertyInfo) child.Member);
                        if (ownerProperty != null)
                            ownerProperty.DynamicInvoke(childInstance, instance);
                    }
                }
            }

            return instance;
        }
        

        private bool IsCached(SurrogateKey hashCode, Type targetType)
        {
            if (!_cache.ContainsKey(targetType))
                return false;
            if (!_cache[targetType].ContainsKey(hashCode))
                return false;
            return true;
        }


        private void AddToCache(SurrogateKey surrogateKey, object instance, Type type)
        {        
            if (!_cache.ContainsKey(type))
                _cache.Add(type,new Dictionary<SurrogateKey, object>());
            _cache[type].Add(surrogateKey,instance);
        }


        private SurrogateKey CreatePrimarySurrogateKey(IDataRecord dataRecord, IProjectionTarget_old projectionTarget)
        {
            var primaryKeyColumns = projectionTarget.PrimaryKeyColumnNames;
            object[] values = GetKeyValues(dataRecord, primaryKeyColumns);
            if (values == null)
                return null;
            var surrogateKey = new SurrogateKey(values);
            return surrogateKey;
        }


       
        private object[] GetKeyValues(IDataRecord dataRecord, string[] columnNames)
        {
            var values = new object[columnNames.Length];
            for (int i = 0; i < columnNames.Length; i++)
            {
                var columnName = columnNames[i];
                var value = dataRecord[columnName];
                if (value == DBNull.Value)
                    return null;
                values[i] = value;
            }
            return values;
        }
                

        private object CreateInstance(Type type)
        {            
            object instance;
            Type concreteType;
            if (!_constructors.ContainsKey(type))
            {
                if (type.IsInterface)
                {
                    var sampleInstance = _serviceContainer.GetService(type);
                    concreteType = sampleInstance.GetType();
                }
                else
                {
                    concreteType = type;
                }

                var constructorInfo = concreteType.GetConstructor(Type.EmptyTypes);
                _constructors.Add(type,constructorInfo);                                    
            }
                            
            instance = _constructors[type].DynamicInvoke();                
            return instance;
        
        }

      




        public void Initialize(IServiceContainer source)
        {
            _serviceContainer = source;
        }
    }
}
